﻿using System.Collections.Concurrent;
using System.ComponentModel;
using System.Linq.Expressions;
using System.Reflection;
using Swimj.Core.Converter;
using Swimj.Core.Extensions;

namespace Swimj.Core.Utilities
{
    public class ClassMappingSettings
    {
        private static ClassMappingSettings defaults;

        private static readonly Dictionary<Assembly, List<TypeConverter>> assembliesTypeConverters =
            new Dictionary<Assembly, List<TypeConverter>>();

        internal static ConcurrentBag<TypeConverter> GlobalConverters = new ConcurrentBag<TypeConverter>();
        internal readonly List<MemberInfo> PropertiesToIgnore = new List<MemberInfo>();

        internal readonly List<KeyValuePair<MemberInfo, MemberInfo>> PropertiesToAssign =
            new List<KeyValuePair<MemberInfo, MemberInfo>>();

        internal static ClassMappingSettings CurrentSettings { get; private set; }

        #region Global Static functions

        public static void ClearGlobalConverters()
        {
            GlobalConverters = new ConcurrentBag<TypeConverter>();
        }

        public static ClassMappingSettings RemoveGlobalConverter(TypeConverter converter)
        {
            if (converter != null && GlobalConverters.Contains(converter))
            {
                var list = new List<TypeConverter>(GlobalConverters.ToArray());
                if (list.Contains(converter))
                {
                    list.Remove(converter);
                    GlobalConverters = new ConcurrentBag<TypeConverter>(list);
                }
            }

            return (CurrentSettings ?? Default).RemoveConverter(converter);
        }

        public static void AddGlobalConverters(params TypeConverter[] converters)
        {
            foreach (var converter in converters)
                GlobalConverters.Add(converter);
        }

        public static TypeConverter AddGlobalConverter<TIn, TOut>(Func<TIn, TOut> fn = null,
            bool allowAssignableInputs = false)
        {
            GenericTypeConverter<TIn, TOut> converter = new GenericTypeConverter<TIn, TOut>(null, allowAssignableInputs);
            converter.SetConverterFunc(fn ?? (m => m.MapTo<TOut>()));
            AddGlobalConverters(converter);
            return converter;
        }

        public static TypeConverter AddGlobalConverter(Type tIn, Type tOut, Func<object, object> fn = null,
            bool allowAssignableInputs = false)
        {
            SimpleFuncConverter converter = new SimpleFuncConverter(tIn, tOut, null, allowAssignableInputs);
            converter.SetConverterFunc(fn ?? (m => m.MapTo(tOut)));
            AddGlobalConverters(converter);
            return converter;
        }

        #endregion


        public static ClassMappingSettings Default => defaults ?? new ClassMappingSettings();

        public static ClassMappingSettings Fast
        {
            get
            {
                return new ClassMappingSettings(true)
                    .Set(s => s.IgnoreExceptions = true,
                        s => s.AutoCheckForDataContractJsonSerializer = false,
                        s => s.ShouldEnumeratePropertiesAsync = true,
                        s => s.TryContainerResolve = false,
                        s => s.SearchForTryParseInTargetTypes = false
                    );
            }
        }

        public ClassMappingSettings SetAsDefault()
        {
            defaults = this;
            return this;
        }

        public ClassMappingSettings IgnoreProperties<TInput>(params Expression<Func<TInput, object>>[] toIgnore)
        {
            toIgnore.Apply(expression => PropertiesToIgnore.Add(expression.GetMemberInfo()));
            return this;
        }

        public ClassMappingSettings IgnoreProperties(params MemberInfo[] toIgnore)
        {
            PropertiesToIgnore.AddRange(toIgnore);
            return this;
        }


        public bool HasAssignments { get; private set; }

        public int MinListCountToEnumerateAsync { get; set; }

        public bool TryContainerResolve { get; set; }

        public bool IgnoreGlobalConverters { get; set; }

        public bool DefaultValueTypeValuesAsNullForNonValueTypes { get; set; }

        public bool AllowGuidConversion { get; set; }

        public bool MatchCaseForEnumNameConversion { get; set; }

        public bool SearchForTryParseInTargetTypes { get; set; }

        public bool IgnoreExceptions { get; set; }

        public bool IncludePrivateFields { get; set; }

        public bool CoverUpAbstractMembers { get; set; }

        internal ConcurrentBag<TypeConverter> TypeConverters { get; private set; }

        public bool ShouldEnumerateListsAsync { get; set; }

        public bool ObjectToStringWithJSON { get; set; }

        public bool CanConvertFromJSON { get; set; }

        public bool AutoCheckForDataContractJsonSerializer { get; set; }

        public bool ShouldEnumeratePropertiesAsync { get; set; }

        public IServiceProvider ServiceProvider { get; set; }

        public ClassMappingSettings(bool shouldEnumerateListsAsync = false,
            params TypeConverter[] specificConverters)
        {
            CanConvertFromJSON = true;
            AutoCheckForDataContractJsonSerializer = true;

            CurrentSettings = this;
            AllowGuidConversion = true;
            MinListCountToEnumerateAsync = 100;
            SearchForTryParseInTargetTypes = true;
            ShouldEnumerateListsAsync = shouldEnumerateListsAsync;
            TypeConverters = new ConcurrentBag<TypeConverter>(specificConverters);
            ShouldEnumeratePropertiesAsync = !System.Diagnostics.Debugger.IsAttached;
            CoverUpAbstractMembers = false;
            IncludePrivateFields = false;
            MatchCaseForEnumNameConversion = false;
            AddDefaultConverters();
        }

        private void AddDefaultConverters()
        {
            AddConverter<DateOnly, DateTime>(only => only.ToDateTime(new TimeOnly(0, 0)));
            AddConverter<DateTime, DateOnly>(DateOnly.FromDateTime);

            AddConverter<TimeOnly, DateTime>(only => new DateTime(DateTime.MinValue.Year, DateTime.MinValue.Month,
                DateTime.MinValue.Day, only.Hour, only.Minute, only.Second));
            AddConverter<DateTime, TimeOnly>(TimeOnly.FromDateTime);

            AddConverter<TimeOnly, TimeSpan>(only => only.ToTimeSpan());
            AddConverter<TimeSpan, TimeOnly>(TimeOnly.FromTimeSpan);
        }

        public ClassMappingSettings RemoveConverters(params TypeConverter[] converters)
        {
            converters.Apply(c => RemoveConverter(c));
            return this;
        }

        public ClassMappingSettings RemoveConverter(TypeConverter converter)
        {
            if (converter != null && TypeConverters != null && TypeConverters.Contains(converter))
            {
                var list = new List<TypeConverter>(TypeConverters.ToArray());
                if (list.Contains(converter))
                {
                    list.Remove(converter);
                    TypeConverters = new ConcurrentBag<TypeConverter>(list);
                }
            }

            return this;
        }

        public ClassMappingSettings AddTypeMapping<TIn, TOut>(bool allowAssignableInputs = false)
        {
            return AddConverter<TIn, TOut>(null, allowAssignableInputs);
        }

        public ClassMappingSettings AddAssignment<TInput, TResult>(Expression<Func<TInput, object>> inProp,
            Expression<Func<TResult, object>> outProp)
        {
            return AddAssignment(inProp.GetMemberInfo(), outProp.GetMemberInfo());
        }

        public ClassMappingSettings AddAssignment<TResult>(MemberInfo inputProperty,
            Expression<Func<TResult, object>> outProp)
        {
            return AddAssignment(inputProperty, outProp.GetMemberInfo());
        }

        public ClassMappingSettings AddAssignment(MemberInfo inputProperty, MemberInfo outputProperty)
        {
            HasAssignments = true;
            PropertiesToAssign.Add(new KeyValuePair<MemberInfo, MemberInfo>(inputProperty, outputProperty));
            return IgnoreProperties(inputProperty);
        }

        public ClassMappingSettings AddConverters(params TypeConverter[] converters)
        {
            foreach (var converter in converters)
                TypeConverters.Add(converter);
            return this;
        }

        public ClassMappingSettings AddConverter(TypeConverter converter)
        {
            TypeConverters.Add(converter);
            return this;
        }

        public ClassMappingSettings AddConverter<TIn, TOut>(Func<TIn, TOut> fn = null, bool allowAssignableInputs = false)
        {
            GenericTypeConverter<TIn, TOut> converter = new GenericTypeConverter<TIn, TOut>(null, allowAssignableInputs);
            converter.SetConverterFunc(fn ?? (m =>
                m.MapTo<TOut>(RemoveConverter(converter)))); // Eigenen converter eintfernen (sonst droht stackoverflow)
            return AddConverter(converter);
        }

        public ClassMappingSettings AddConverter(Type tIn, Type tOut, Func<object, object> fn = null,
            bool allowAssignableInputs = false)
        {
            SimpleFuncConverter converter = new SimpleFuncConverter(tIn, tOut, null, allowAssignableInputs);
            converter.SetConverterFunc(fn ?? (m =>
                m.MapTo(tOut, RemoveConverter(converter)))); // Eigenen converter eintfernen (sonst droht stackoverflow)
            return AddConverter(converter);
        }

        public ClassMappingSettings AddAllLoadedTypeConverters()
        {
            foreach (TypeConverter converter in RetrieveLoadedAssembliesTypeConverters())
                TypeConverters.Add(converter);
            return this;
        }

        public IList<TypeConverter> RetrieveLoadedAssembliesTypeConverters()
        {
            var assemblies = AppDomain.CurrentDomain.GetAssemblies()
                .Where(assembly => assembly.GetName().Version > new Version(0, 0, 0, 0)).ToList();
            assemblies.Add(typeof(BooleanConverter).Assembly);

            foreach (Assembly assembly in assemblies)
            {
                if (!assembliesTypeConverters.ContainsKey(assembly))
                {
                    try
                    {
                        var currentAssemblyConverterList = new List<TypeConverter>();
                        foreach (Type converterType in assembly.GetTypes()
                                     .Where(type => typeof(TypeConverter).IsAssignableFrom(type)))
                        {
                            try
                            {
                                TypeConverter typeConverter =
                                    ReflectionHelper.CreateInstance(converterType, false, false) as TypeConverter;
                                if (typeConverter != null)
                                {
                                    currentAssemblyConverterList.Add(typeConverter);
                                }
                            }
                            catch
                            {
                            }
                        }

                        assembliesTypeConverters.Add(assembly, currentAssemblyConverterList);
                    }
                    catch
                    {
                    }
                }
            }

            return assembliesTypeConverters.Values.SelectMany(list => list).ToList();
        }
    }
}